package cn.fulong.sso.client.filter;


import cn.fulong.sso.client.constant.SSOConstants;
import cn.fulong.sso.client.manager.SessionManager;
import cn.fulong.sso.client.utils.*;

import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;


/**
 * @author 云树
 * Created on 2018/11/9
 */
public abstract class AbstractSSOClientFilter<T> implements Filter {
    /**
     * 内置白名单请求，一定不能拦截
     **/
    protected Set<String> EXCLUDE_SERVLET = new HashSet<>(Arrays.asList("/logout", "/sso_logout"));
    /**
     * sso.client 唯一标识【必须配置】
     **/
    protected String appid;
    /**
     * 外网sso.server服务地址【必须配置】
     **/
    protected String publicSsoServer;
    /**
     * 内网sso.server服务地址（若内网环境不通）【可不配置】
     **/
    protected String localSsoServer;
    /**
     * 排除url表达式
     */
    private Set<String> excludesPattern;
    /**
     * 需要去拉去用户信息
     */
    private Set<String> includesPattern;
    /**
     * 是否验证syn与ack中的seq值，以达到每次刷新，都会去获取用户登录状态，结合includesPattern使用
     **/
    protected boolean verifySynAckSeq = false;
    /**
     * authc表示需要认证 没有进行身份认证是不能进行访问的
     */
    private Set<String> authcUrlsPattern;
    /**
     * anon 表示匿名访问，不需要认证以及授权
     */
    private Set<String> anonUrlsPattern;
    /**
     * 获取用户基本登录信息api
     **/
    protected String memberRestApiUrl;

    /**
     * 授权地址
     */
    protected String authUrl;
    /**
     * 本地会话管理器（做退出，清理本地token绑定session用）
     **/
    protected SessionManager sessionManager;

    /**
     * ajax拦截json格式字符串
     */
    protected String ajaxJsonMessage = "{\"msg\":\"会话超时，请重新登录！\",\"code\":\"lose_time\",\"data\":\"%s\"}";

    /**
     * 根据access_code获取用户授权信息
     *
     * @param accessCode accessSsoServer（）方法返回值
     * @return
     * @throws
     */

    protected abstract T getAuthInfo(String accessCode, HttpServletRequest httpRequest, HttpServletResponse response);

    /**
     * @param httpRequest
     * @return 是否需要去授权获取登录信息, 若是登录成功跳转，会添加此标识符
     */
    protected boolean ifNeedAuth(HttpServletRequest httpRequest) {
        String localState = httpRequest.getParameter(SSOConstants.ServerUrlParams.AUTH);
        return StringSelfUtil.isNotBlank(localState);
    }

    /**
     * @param httpRequest
     * @return 校验state参数防止挟持(用于防止csrf攻击)
     */
    protected boolean verifyState(HttpServletRequest httpRequest) {
        HttpSession httpSession = httpRequest.getSession();
        return this.verifyState(httpRequest, httpSession);
    }

    /**
     * @param httpRequest
     * @param httpSession
     * @return校验state参数防止挟持(用于防止csrf攻击)
     */
    protected boolean verifyState(HttpServletRequest httpRequest, HttpSession httpSession) {
        String localState = (String) httpSession.getAttribute(SSOConstants.Session.STATE);
        if (StringSelfUtil.isBlank(localState)) {
            return false;
        }
        String state = httpRequest.getParameter(SSOConstants.ServerUrlParams.STATE);
        httpSession.removeAttribute(SSOConstants.Session.STATE);
        return localState.equals(state);
    }

    /**
     * 获取sso.server签发的access_code
     *
     * @param httpRequest
     * @return
     * @throws IOException
     */
    protected String accessSsoServer(HttpServletRequest httpRequest) {
        String access_code = httpRequest.getParameter(SSOConstants.ServerUrlParams.ACCESS_CODE);
        return access_code;
    }


    /**
     * @return 是否已去sso服务器验证登录状态，若 verifySynAckSeq =true（verify_syn_ack_seq配置）
     * 则验证sso.server，ack与本地syn 的seq值是否相等，实现本页面刷新验证
     */
    protected boolean hadAckSsoServerLoginStatus(HttpServletRequest httpRequest) {
        String ack = httpRequest.getParameter(SSOConstants.ServerUrlParams.ACK);
        if (!this.verifySynAckSeq) {
            return StringSelfUtil.isNotBlank(ack);
        }
        /**根据本地存储的syn_seq序列号，判断是否需要重新去sso授权 逻辑 begin**/
        HttpSession session = httpRequest.getSession();
        if (StringSelfUtil.isBlank(ack)) {
            writeNextSynSeq(session, 0);
            return false;
        }
        if (!StringSelfUtil.isNumeric(ack)) {
            writeNextSynSeq(session, 0);
            return false;
        }
        Integer seq = this.getSynSeq(session);
        if (seq.equals(Integer.valueOf(ack))) {
            writeNextSynSeq(session, seq);
            return true;
        }
        return false;
        /**根据本地存储的syn_seq序列号，判断是否需要重新去sso授权 逻辑 end**/
    }

    /**
     * @param session
     * @return 获取客户端存储的序列号
     */
    protected Integer getSynSeq(HttpSession session) {
        if (!this.verifySynAckSeq) {
            return SSOConstants.Syn.SEQ_MIN;
        }
        Integer seq = (Integer) session.getAttribute(SSOConstants.Syn.SEQ_SESSION);
        if (null == seq) {
            session.setAttribute(SSOConstants.Syn.SEQ_SESSION, SSOConstants.Syn.SEQ_MIN);
            return SSOConstants.Syn.SEQ_MIN;
        }
        return seq;
    }

    /**
     * @param session
     * @param currentSeq
     * @return
     * @desc 客户端session存储下次syn序列号
     */
    protected Integer writeNextSynSeq(HttpSession session, Integer currentSeq) {
        Integer nextSeqlNum = currentSeq != null ? (currentSeq >= SSOConstants.Syn.SEQ_MAX ? SSOConstants.Syn.SEQ_MIN : currentSeq + 1) : SSOConstants.Syn.SEQ_MIN;
        session.setAttribute(SSOConstants.Syn.SEQ_SESSION, nextSeqlNum);
        return nextSeqlNum;
    }


    /**
     * @param httpServletRequest
     * @param httpResponse
     * @throws IOException
     * @desc 携带syn，from参数重定向去验证sso登录状态 （配套isIncludesSyn使用）
     */
    protected void redirectSynAuthStatusUrl(HttpServletRequest httpServletRequest, HttpServletResponse httpResponse) throws IOException {
        String verifUrl = generateVerifyRedirecrUrl(httpServletRequest);
        String from = httpServletRequest.getRequestURL().toString();
        String queryStr = httpServletRequest.getQueryString();
        if (StringSelfUtil.isNotBlank(queryStr)) {
            from = from + "?" + httpServletRequest.getQueryString();
        }
        from = Base64Util.encode(from);
        String redirectUrl = (new StringBuilder()).append(verifUrl).append("&syn=").append(this.getSynSeq(httpServletRequest.getSession())).append("&from=").append(from).toString();
        httpResponse.sendRedirect(redirectUrl);
    }

    /**
     * @param httpServletRequest
     * @param httpResponse
     * @throws IOException
     * @desc 重定向到sso验证客户端登录状态，必须登录链接 （配套isAuthc）
     */
    protected void redirectSsoVerifyUrl(HttpServletRequest httpServletRequest, HttpServletResponse httpResponse) throws IOException {
        String verifUrl = generateVerifyRedirecrUrl(httpServletRequest);
        if (RequestUtil.isAjax(httpServletRequest)) {
            httpResponse.setContentType("application/json;charset=utf-8");
            String referer = RequestUtil.getReferer(httpServletRequest);
            PrintWriter writer = null;
            try {
                writer = httpResponse.getWriter();
                writer.write(String.format(ajaxJsonMessage, (verifUrl + "&from=" + Base64Util.encode(referer))));
            } finally {
                if (writer != null) {
                    writer.flush();
                    writer.close();
                }
            }
        } else {
            String from = "";
            if (RequestUtil.isMethodPost(httpServletRequest)) {
                //若为post请求，获取请求来源，作为from链接地址
                String referer = RequestUtil.getReferer(httpServletRequest);
                if (StringSelfUtil.isNotBlank(referer)) {
                    from = referer;
                }
            } else {
                from = httpServletRequest.getRequestURL().toString();
                String queryStr = httpServletRequest.getQueryString();
                if (StringSelfUtil.isNotBlank(queryStr)) {
                    from = from + "?" + httpServletRequest.getQueryString();
                }
            }
            from = Base64Util.encode(from);
            httpResponse.sendRedirect(verifUrl + "&from=" + from);
        }
    }

    /**
     * @param httpRequest
     * @return 构建验证sso服务器url
     */
    protected String generateVerifyRedirecrUrl(HttpServletRequest httpRequest) {
        /**用于保持请求和回调的状态，授权请求后原样带回给第三方。
         * 该参数可用于防止csrf攻击（跨站请求伪造攻击），建议第三方带上该参数，可设置为简单的随机数加session进行校验*/
        String state = bindState(httpRequest);
        return new StringBuilder(this.authUrl).append("&").append(SSOConstants.ClientParams.STATE).append("=").append(state).toString();
    }

    /**
     * @param httpRequest
     * @return 绑定并生成随机码，作为state的携带参数
     */
    protected String bindState(HttpServletRequest httpRequest) {
        String state = String.valueOf(new Random().nextInt(999999));
        httpRequest.getSession().setAttribute(SSOConstants.Session.STATE, state);
        return state;
    }

    /**
     * @param httpRequest
     * @return
     * @desc 排除url
     */
    protected boolean isExclusion(HttpServletRequest httpRequest) {
        String patternUrl = this.getPatternUrl(httpRequest);
        return this.isMatches(this.excludesPattern, patternUrl);
    }


    /**
     * @param httpRequest
     * @return
     * @desc 是否匹配includes规则
     */
    protected boolean isIncludesPattern(HttpServletRequest httpRequest) {
        String patternUrl = this.getPatternUrl(httpRequest);
        return this.isMatches(this.includesPattern, patternUrl);
    }

    /**
     * @param httpRequest
     * @return
     * @desc 需要去拉同步用户信息，但不必登录
     */
    protected boolean isIncludesSyn(HttpServletRequest httpRequest) {
        /**排除ajax请求及post请求**/
        if (RequestUtil.isAjax(httpRequest) || RequestUtil.isMethodPost(httpRequest)) {
            return false;
        }
        /**已经去sso.server验证过登录状态**/
        if (this.hadAckSsoServerLoginStatus(httpRequest)) {
            return false;
        }
        return this.isIncludesPattern(httpRequest);
    }


    /**
     * @param httpRequest
     * @return
     * @desc authc表示需要认证 没有进行身份认证是不能进行访问的
     */
    protected boolean isAuthc(HttpServletRequest httpRequest) {
        String patternUrl = this.getPatternUrl(httpRequest);
        return this.isMatches(this.authcUrlsPattern, patternUrl);
    }

    /**
     * @param httpRequest
     * @return
     * @desc anon 表示匿名访问，不需要认证以及授权
     */
    protected boolean isAnon(HttpServletRequest httpRequest) {
        String patternUrl = this.getPatternUrl(httpRequest);
        return this.isMatches(this.anonUrlsPattern, patternUrl);
    }


    protected String getContextPath(HttpServletRequest httpRequest) {
        String contextPath = httpRequest.getContextPath();
        if (contextPath == null || contextPath.length() == 0) {
            contextPath = "/";
        }
        return contextPath;
    }


    protected String getPatternUrl(HttpServletRequest httpRequest) {
        String servletPath = httpRequest.getServletPath();
        if (servletPath.indexOf("/show.action") == 0) {
            //走框架查询，添加code匹配
            String code = httpRequest.getParameter("code");
            servletPath = servletPath + "?code=" + code;
        }
        return servletPath;
    }


    protected boolean isMatches(Set<String> setPattern, String servletPath) {
        if (setPattern == null || servletPath == null) {
            return false;
        }
        if (setPattern.contains(servletPath)) {
            //对全值匹配使用hash查找，加快查找速度，亲测，对全值匹配差不多有20倍性能提升
            return true;
        }
        for (String pattern : setPattern) {
            if (this.matches(pattern, servletPath)) {
                return true;
            }
        }
        return false;
    }


    /**
     * @param pattern 过滤规则 : *aaa  以aaa结尾
     *                bbb* 以bbb开头
     *                aaa*bbb*ccc 以aaa开头 并且以ccc结尾 （注：中间的bbb不会做匹配校验，诚意为url校验规则
     *                ，没必要这么复杂，否则只会让代码更加复杂）
     *                ddd ：全匹配ddd字符串
     * @param source
     * @return
     * @Referer druid
     */
    protected boolean matches(String pattern, String source) {
        if (pattern == null || source == null) {
            return false;
        }
        if (pattern.endsWith("*")) {
            // pattern: /test* source:/test/index.html
            int length = pattern.length() - 1;
            if (source.length() >= length) {
                if (pattern.substring(0, length).equals(
                        source.substring(0, length))) {
                    return true;
                }
            }
        } else if (pattern.startsWith("*")) {
            // pattern: *.html source:/xx/xx.html
            int length = pattern.length() - 1;
            if (source.length() >= length
                    && source.endsWith(pattern.substring(1))) {
                return true;
            }
        } else if (pattern.contains("*")) {
            // pattern:  /test/*/index.html source:/test/admin/index.html
            int start = pattern.indexOf("*");
            int end = pattern.lastIndexOf("*");
            if (source.startsWith(pattern.substring(0, start))
                    && source.endsWith(pattern.substring(end + 1))) {
                return true;
            }
        } else {
            // pattern: /test/index.html source:/test/index.html
            if (pattern.equals(source)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void init(FilterConfig fConfig) throws ServletException {
        this.appid = ServletConfigUtil.getConfig(fConfig, SSOConstants.ClientConfig.APP_ID);
        Assert.hasText(this.appid, "appId:配置不能为空");
        this.publicSsoServer = ServletConfigUtil.getSSOPublicService(fConfig);
        Assert.hasText(this.publicSsoServer, "public_sso_server:配置不能为空");
        String localSsoUrl = ServletConfigUtil.getConfig(fConfig, SSOConstants.ClientConfig.LOCAL_SSO_SERVER);
        if (StringSelfUtil.isNotBlank(localSsoUrl)) {
            this.localSsoServer = SsoUrlUtil.getRootDomain(localSsoUrl);
        } else {
            this.localSsoServer = this.publicSsoServer;
        }
        this.authUrl = SsoUrlUtil.mergeUrl(this.publicSsoServer, "auth?appid=" + appid);
        this.memberRestApiUrl = SsoUrlUtil.mergeUrl(this.localSsoServer, SSOConstants.Api.AUTH_INFO_URL_SUFFIX);
        ServletContext servletContext = fConfig.getServletContext();
        this.sessionManager = SessionManagerUtils.getRegisterSessionMangerBean(servletContext);
        Assert.notNull(this.sessionManager, "未添加配置sessionManager bean");
        //排除 url 表达式,
        this.excludesPattern = new HashSet<>(EXCLUDE_SERVLET);
        this.excludesPattern.addAll(ServletConfigUtil.getSets(fConfig, SSOConstants.ClientConfig.EXCLUSIONS));
        //需要去拉去用户信息，但不必登录
        this.includesPattern = ServletConfigUtil.getSets(fConfig, SSOConstants.ClientConfig.INCLUDES);
        //表示必须要认证以及授权
        this.authcUrlsPattern = ServletConfigUtil.getSets(fConfig, SSOConstants.ClientConfig.AUTHC);
        //表示匿名访问，不需要认证以及授权
        this.anonUrlsPattern = ServletConfigUtil.getSets(fConfig, SSOConstants.ClientConfig.ANON);
        //是否验证syn与ack中的seq值，以达到每次刷新，都会去获取用户登录状态，结合includes使用
        this.verifySynAckSeq = ServletConfigUtil.getBoolean(fConfig, "verify_syn_ack_seq");
    }
}
